package cn.zxbetter.secretfile;

import cn.zxbetter.secretfile.config.SecretConfig;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Objects;
import java.util.Properties;
import java.util.UUID;

/**
 * 程序入口
 *
 * @author devin
 */
@Getter
@Setter
@Slf4j
public class Application {

    /**
     * 配置文件名称
     */
    private static final String CONFIG_FILE_NAME = "secret.properties";

    /**
     * 映射文件名称
     */
    private static final String MAP_FILE_NAME = "6ad87d84cda8477bb5acb0d9134bcf8e";

    /**
     * 加密方式
     */
    private static final String ENCRYPT_TYPE = "AES";

    /**
     * 换行符
     */
    private static final String SEPARATOR;

    static {
        SEPARATOR = File.separator;
    }

    /**
     * 工作目录
     */
    private String workplace;

    /**
     * 配置项
     */
    private SecretConfig config;

    /**
     * 处理的文件个数
     */
    private int fileCount = 0;

    public static void main(String[] args) {
        new Application().run(args);
    }

    private void run(String[] args) {

        LocalDateTime startTime = LocalDateTime.now();

        // step0: 解析参数中指定的文件
        log.info("====> 解析参数");
        File file = this.getFileFromArgs(args);
        this.setWorkplace(file.isDirectory() ? file.getAbsolutePath() : file.getParent());

        // step1: 读取配置文件
        log.info("====> 解析配置文件");
        this.setConfig(this.parseConfig());

        // step2: 读取映射文件 6ad87d84cda8477bb5acb0d9134bcf8e
        log.info("====> 读取映射文件");
        File mapFile = new File(this.formatPath(MAP_FILE_NAME) + this.config.getSecretFileSuffix());
        boolean isDecrypt = mapFile.exists();

        // step3: 加密解密文件
        if (isDecrypt) {
            // step3.1: 读取到映射文件，则解密文件
            log.info("====> 开始执行解密程序: {}", this.workplace);
            Properties mapProperties = this.parseMapFile(mapFile);
            this.decryptEntrance(file, mapProperties);
            this.cleanMapFile();
        } else {
            // step3.2: 未读取到映射文件，则加密文件
            log.info("====> 开始执行加密程序: {}", this.workplace);
            Properties mapProperties = new Properties();
            this.encryptEntrance(file, mapProperties);
            this.saveMapFile(mapProperties);
        }

        log.info("<==== 执行完成，处理 {} 个文件，耗时: {} 毫秒", fileCount, ChronoUnit.MILLIS.between(startTime, LocalDateTime.now()));
    }

    private void cleanMapFile() {
        if (fileCount > 0) {
            String mapFilePath = this.formatPath(MAP_FILE_NAME);
            String secretMapFilePath = mapFilePath + this.config.getSecretFileSuffix();

            log.info("====> 清除映射文件: {}", mapFilePath);
            log.info("====> 清除映射文件: {}", secretMapFilePath);

            this.deleteFiles(mapFilePath, secretMapFilePath);
        }
    }

    private void saveMapFile(Properties mapProperties) {
        if (!mapProperties.isEmpty()) {
            String mapFilePath = this.formatPath(MAP_FILE_NAME);

            log.info("====> 生成映射文件: {}", mapFilePath);

            this.deleteFiles(mapFilePath);

            try (FileOutputStream outputStream = new FileOutputStream(mapFilePath)) {
                mapProperties.store(outputStream, "配置文件");
            } catch (IOException e) {
                e.printStackTrace();
            }

            this.encrypt(new File(mapFilePath), MAP_FILE_NAME + this.config.getSecretFileSuffix());

            this.deleteFiles(mapFilePath);
        }
    }

    private Properties parseMapFile(File mapFile) {

        this.decrypt(mapFile, MAP_FILE_NAME);

        Properties properties = new Properties();
        try (FileInputStream inputStream = new FileInputStream(this.formatPath(MAP_FILE_NAME))) {
            properties.load(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (properties.isEmpty()) {
            throw new RuntimeException("读取映射文件失败");
        }

        return properties;
    }

    /**
     * 解密文件
     *
     * @param file 文件或目录
     */
    private void decryptEntrance(File file, Properties mapProperties) {
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            if (files != null && files.length > 0) {

                log.info("-->> 开始解密目录: {}", file.getAbsolutePath());

                for (File childFile : files) {
                    decryptEntrance(childFile, mapProperties);
                }

                log.info("<<-- 结束解密目录");
            }
        } else {
            String secretFileName = file.getName();
            String fileName = mapProperties.getProperty(secretFileName);

            if (Objects.nonNull(fileName) && fileName.length() > 0) {

                log.info("---> 解密文件，加密文件名: {}, 解密后的文件名: {}", secretFileName, fileName);

                this.decrypt(file, fileName);
                file.delete();

                fileCount++;
            }
        }
    }

    private void decrypt(File source, String targetName) {
        byte[] buffer = new byte[8192];
        int i;
        try (FileInputStream inputStream = new FileInputStream(source);
             FileOutputStream outputStream = new FileOutputStream(source.getParent() + SEPARATOR + targetName);
             CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, this.getCipher(Cipher.DECRYPT_MODE))){
            while ((i = inputStream.read(buffer)) != -1) {
                cipherOutputStream.write(buffer, 0, i);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 加密文件
     *
     * @param file 文件或目录
     */
    private void encryptEntrance(File file, Properties mapProperties) {
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            if (files != null && files.length > 0) {

                log.info("-->> 开始加密目录: {}", file.getAbsolutePath());

                for (File childFile : files) {
                    encryptEntrance(childFile, mapProperties);
                }

                log.info("<<-- 结束加密目录");
            }
        } else {
            String sourceName = file.getName();
            if (!this.checkFileName(sourceName)) {
                String secretFileName = uuid() + this.config.getSecretFileSuffix();
                mapProperties.setProperty(secretFileName, sourceName);

                log.info("---> 加密文件，原文件名: {}, 加密后的文件名: {}", sourceName, secretFileName);

                this.encrypt(file, secretFileName);
                file.delete();

                fileCount++;
            }
        }
    }

    private boolean checkFileName(String sourceName) {
        return sourceName.startsWith(".") || sourceName.endsWith(this.config.getSecretFileSuffix()) || Arrays.asList(CONFIG_FILE_NAME).contains(sourceName);
    }

    private void encrypt(File source, String targetName) {
        byte[] buffer = new byte[8192];
        int i;
        try (FileInputStream inputStream = new FileInputStream(source);
             FileOutputStream outputStream = new FileOutputStream(source.getParent() + SEPARATOR + targetName);
             CipherInputStream cipherInputStream = new CipherInputStream(inputStream, this.getCipher(Cipher.ENCRYPT_MODE))){
            while ((i = cipherInputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, i);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private Cipher getCipher(int mode) {
        // 生成密钥
        SecretKey secretKey = new SecretKeySpec(this.config.getSecretKey().getBytes(), ENCRYPT_TYPE);
        Cipher cipher = null;
        try {
            // 生成cipher对象，cipher实际完成加解密操作
            cipher = Cipher.getInstance(ENCRYPT_TYPE);
            // 初始化cipher
            cipher.init(mode, secretKey);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
            e.printStackTrace();
        }
        return cipher;
    }

    /**
     * 解析配置文件
     *
     * @return 配置
     */
    private SecretConfig parseConfig() {
        File file = this.getFile(formatPath(CONFIG_FILE_NAME));
        Properties properties = new Properties();

        try (FileInputStream inputStream = new FileInputStream(file)) {
            properties.load(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return SecretConfig.from(properties);
    }

    private File getFileFromArgs(String[] args) {
        if (args == null || args.length == 0) {
            throw new RuntimeException("未指定文件路径");
        }

        // 只读取第一参数作为文件路径
        return this.getFile(args[0]);
    }

    private File getFile(String fileName) {
        File file = new File(fileName);
        if (!file.exists()) {
            throw new RuntimeException("文件或目录不存在: " + fileName);
        }

        return file;
    }

    /**
     * 删除文件
     *
     * @param paths 文件路径
     */
    private void deleteFiles(String... paths) {
        for (String path : paths) {
            File file = new File(path);
            if (file.exists()) {
                file.delete();
            }
        }
    }

    private String formatPath(String path) {
        return this.workplace + SEPARATOR + path;
    }

    /**
     * 生成UUID，去掉横线
     *
     * @return 去掉横线的UUID
     */
    private static String uuid() {
        return UUID.randomUUID().toString().replace("-", "");
    }
}
